package net.funtip.ormgenerator.parser;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import net.funtip.ormgenerator.generator.TypeMapperSimpleImpl;
import net.funtip.ormgenerator.model.Association;
import net.funtip.ormgenerator.model.Attribute;
import net.funtip.ormgenerator.model.Entity;
import net.funtip.ormgenerator.model.Finder;
import net.funtip.ormgenerator.model.Finder.FinderType;
import net.funtip.ormgenerator.model.FinderOperation;
import net.funtip.ormgenerator.model.FinderOperation.FinderOperationType;
import net.funtip.ormgenerator.model.Method;
import net.funtip.ormgenerator.model.Model;
import net.funtip.ormgenerator.model.OrderBy;
import net.funtip.ormgenerator.model.OrderBy.OrderDirection;
import net.funtip.ormgenerator.model.UniqueConstraint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class ModelParser extends DefaultHandler {
	
	public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

	private Logger log = LoggerFactory.getLogger(this.getClass());
	
	public enum ElementType {model, entity, attributes, attribute, calculatedValue,
		serializable, implementsInterface, association, identifier, idGenerator, method, methodSignature, methodBody,
		constraints, uniqueConstraint, attributeRef, finder, finderOperation, orderBy, 
		_SKIP};

	protected Stack<ElementType> tags;
	protected Stack<Map<String, String>> attributes;
	protected StringBuffer value;
	protected Model currentModel;
	protected Entity currentEntity;
	protected Attribute currentAttribute;
	protected Method currentMethod;
	protected UniqueConstraint currentUniqueConstraint;
	protected Finder currentFinder;
	protected Map<String, String> currentAttributes;

	private SimpleDateFormat sdf = new SimpleDateFormat(DATETIME_FORMAT);
	
	public ModelParser() {
		super();
		tags = new Stack<ElementType>();
		attributes = new Stack<Map<String, String>>();
	}
	
	public Model getModel() {
		return currentModel;
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
		value = new StringBuffer();
		ElementType tag;
		currentAttributes = getAttributeMap(attributes);
		try {
			tag = ElementType.valueOf(localName);
			
			handleEntity(tag);
			
			handleOptions(tag);
			
		} catch (IllegalArgumentException e) {
			log.warn("tag not found "+localName);
			tag = ElementType._SKIP;
		}
		this.tags.push(tag);
		this.attributes.push(currentAttributes);
		
	}
	
	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException {
		ElementType r = tags.pop();
		currentAttributes = attributes.size()>0?attributes.pop():null;
		switch (r) {
		case calculatedValue:
			currentAttribute.setCalculatedValue(value.toString());
			break;
		case implementsInterface:
			currentEntity.addImplementedInterface(value.toString());
			break;
		case association:
			currentAttribute.setAssociated(true);
			String associationType = currentAttributes.get("type");
			Entity associatedEntity = getOrCreateEntity(currentModel.getBasePackageName(), currentAttribute.getType(), currentModel);
			Association association = new Association(currentAttribute, associatedEntity, Association.AssociationType.valueOf(associationType));
			break;
		case identifier:
			currentAttribute.setIdentifier(true);
			break;
		case idGenerator:
			currentAttribute.setIdGenerated(true);
			currentAttribute.setGeneratorType(currentAttributes.get("type"));
			currentAttribute.setSequenceName(currentAttributes.get("sequenceName"));
			break;
		case methodSignature:
			currentMethod.setSignature(value.toString());
			break;
		case methodBody:
			currentMethod.setBody(value.toString());
			break;
		case finderOperation:
			Attribute attr = currentEntity.getAttribute(currentAttributes.get("attributeName"));
			String paramName = currentAttributes.get("paramName");
			if (attr != null) {
				FinderOperation fo = new FinderOperation(currentFinder, attr);
				if (paramName != null) {
					fo.setParamName(paramName);
				}
				String finderOperationType = currentAttributes.get("type");
				if (finderOperationType != null) {
					try {
						FinderOperationType fot = FinderOperationType.valueOf(finderOperationType);
						fo.setType(fot);
					} catch (IllegalArgumentException e) {
						log.error("unknown FinderOperationType "+finderOperationType);
					}
				}
			} else {
				log.error("Attribute with name '" + currentAttributes.get("attributeName") + "' not found in entity '" + currentEntity.getName() + "'");
			}
			break;
		case attributeRef:
			attr = currentEntity.getAttribute(currentAttributes.get("name"));
			if (attr != null) {
				currentUniqueConstraint.addAttribute(attr);
			} else {
				log.error("Attribute with name '" + currentAttributes.get("name") + "' not found in entity '" + currentEntity.getName() + "'");
			}
			break;
		case orderBy:
			attr = currentEntity.getAttribute(currentAttributes.get("name"));
			if (attr != null) {
				OrderBy o = new OrderBy(currentFinder, attr);
				o.setDirection("desc".equalsIgnoreCase(currentAttributes.get("direction"))? OrderDirection.desc: OrderDirection.asc);
			} else {
				log.error("Attribute with name '" + value.toString() + "' not found in entity '" + currentEntity.getName() + "'");
			}
			break;
			
		}
	}
	
	private void handleOptions(ElementType r) {
		switch (r) {
		case serializable:
			currentEntity.setSerializable(true);
			break;
		}
	}
	
	private void handleEntity(ElementType r) {
		switch (r) {
		case model:
			currentModel = new Model();
			currentModel.setTypeMapper(new TypeMapperSimpleImpl());
			currentModel.setBasePackageName(currentAttributes.get("basePackageName"));
			break;
		case entity:
			String name = currentAttributes.get("name");
			String packageName = currentAttributes.get("packageName");
			currentEntity = getOrCreateEntity(packageName, name, currentModel);
			currentEntity.setColumnPrefix(currentAttributes.get("columnPrefix"));
			currentEntity.setAbstractSuperclass(getBooleanValue(currentAttributes.get("isAbstractSuperclass")));
			String superClassName = currentAttributes.get("superClass");
			if (superClassName != null && !superClassName.isEmpty()) {
				Entity superClass = currentModel.getEntity(superClassName);
				if (superClass != null) {
					currentEntity.setSuperClass(superClass);
				} else {
					log.error("SuperClass not found for entity "+name + ": superclass: " + superClassName);
				}
			}
			log.info("new entity created "+currentEntity);
			
			currentEntity.setTableName(currentAttributes.get("tableName"));
			break;
		case attribute:
			currentAttribute = new Attribute(currentAttributes.get("type"), currentAttributes.get("name"), currentEntity);
			log.info("new attribute created "+currentAttribute);
			currentAttribute.setColumnName(currentAttributes.get("columnName"));
			currentAttribute.setColumnType(currentAttributes.get("columnType"));
			currentAttribute.setDefaultValue(currentAttributes.get("defaultValue"));
			currentAttribute.setMandatory(getBooleanValue(currentAttributes.get("isMandatory")));
			currentAttribute.setTransient(getBooleanValue(currentAttributes.get("isTransient")));
			currentAttribute.setIncludedInEquals(getBooleanValue(currentAttributes.get("isIncludedInEquals"), true));
			currentAttribute.setUnique(getBooleanValue(currentAttributes.get("isUnique")));
			break;
		case method:
			currentMethod = new Method(currentEntity);
			break;
		case uniqueConstraint:
			currentUniqueConstraint = new UniqueConstraint(currentEntity);
			break;
		case finder:
			currentFinder = new Finder(currentEntity, currentAttributes.get("name"));
			currentFinder.setType("many".equalsIgnoreCase(currentAttributes.get("type"))? FinderType.many: FinderType.single);
			currentFinder.setLimited(getBooleanValue(currentAttributes.get("isLimited")));
			break;
		}
		
	}

	protected int getIntValue(String s, int defaultValue) {
		try {
			return Integer.parseInt(s);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	protected Date getDateValue(String string) {
		try {
			return sdf.parse(string);
		} catch (ParseException e) {
			return null;
		}
	}

	protected Boolean getBooleanValue(String s) {
		return Boolean.parseBoolean(s);
	}

	protected Boolean getBooleanValue(String s, boolean defaultValue) {
		if (s == null) return defaultValue;
		return Boolean.parseBoolean(s);
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException {
		value.append(ch, start, length);
	}

	public void parse(InputStream is) {
		parse(is, this);
	}
	
	private void parse(InputStream is, DefaultHandler handler) {
		String validationFeature = "http://xml.org/sax/features/validation";
		String schemaFeature = "http://apache.org/xml/features/validation/schema";
        
		try {
			
			XMLReader r = XMLReaderFactory.createXMLReader();
			
	        r.setFeature(validationFeature,true);
	        r.setFeature(schemaFeature,true);
	        r.setErrorHandler(handler);
	        r.setContentHandler(handler);

	        long startTime = System.currentTimeMillis();
	        
	        r.parse(new InputSource(is));

	        log.info("Time needed for parsing input stream : "+(System.currentTimeMillis()-startTime)+" ms");
	        if (!tags.isEmpty()) {
				StringBuilder sb = new StringBuilder();
				for (ElementType tag: tags) {
					sb.append(tag).append(" ");
				}
	        	log.warn("XML structure corrupted? "+sb.toString());
	        }
		} catch (SAXException e) {
			throw new RuntimeException(e.getMessage(),e);
		} catch (IOException e) {
			throw new RuntimeException(e.getMessage(),e);
		} catch (Throwable e) {
			StringBuilder sb = new StringBuilder();
			for (ElementType tag: tags) {
				sb.append(tag).append(" ");
			}
			throw new RuntimeException("Error in XML: "+e.getMessage()+". Path: "+sb.toString(), e);
		}
	}

	private Map<String, String> getAttributeMap(Attributes attributes) {
		Map<String, String> map = new HashMap<String, String>();
		for (int i=0; i<attributes.getLength(); i++) {
			map.put(attributes.getLocalName(i), attributes.getValue(i));
		}
		return map;
	}
	
	private Entity getOrCreateEntity(String packageName, String entityName, Model model) {
		// FIXME getOrCreateEntity should use a package name to identify the type
		Entity entity = model.getEntity(entityName);
		if (entity == null) {
			entity = new Entity(packageName, entityName, model);
		}
		return entity;
	}

	@Override
	public void error(SAXParseException e) throws SAXException {
		log.error("Error parsing XML stream in line "+e.getLineNumber()+" column "+e.getColumnNumber()+": "+e.getLocalizedMessage());
		throw e;
	}

	@Override
	public void fatalError(SAXParseException e) throws SAXException {
		log.error("Fatal error parsing XML stream in line "+e.getLineNumber()+" column "+e.getColumnNumber()+": "+e.getLocalizedMessage());
		throw e;
	}

	@Override
	public void warning(SAXParseException e) throws SAXException {
		log.warn("Warning parsing XML stream in line "+e.getLineNumber()+" column "+e.getColumnNumber()+": "+e.getLocalizedMessage());
		super.warning(e);
	}

}
